/*
Copyright (c) 2010,2011,2012,2013,2014 Morgan Roderick http://roderick.dk
License: MIT - http://mrgnrdrck.mit-license.org
https://github.com/mroderick/PubSubJS
*/
(function (root, factory){
  'use strict';

    if (typeof define === 'function' && define.amd){
        // AMD. Register as an anonymous module.
        define(['exports'], factory);

    } else if (typeof exports === 'object'){
        // CommonJS
        factory(exports);

    }

    // Browser globals
    var PubSub = {};
    root.PubSub = PubSub;
    factory(PubSub);
    
}(( typeof window === 'object' && window ) || this, function (PubSub){
  'use strict';

  var messages = {},
    lastUid = -1;

  function hasKeys(obj){
    var key;

    for (key in obj){
      if ( obj.hasOwnProperty(key) ){
        return true;
      }
    }
    return false;
  }

  /**
   *  Returns a function that throws the passed exception, for use as argument for setTimeout
   *  @param { Object } ex An Error object
   */
  function throwException( ex ){
    return function reThrowException(){
      throw ex;
    };
  }

  function callSubscriberWithDelayedExceptions( subscriber, message, data ){
    try {
      subscriber( message, data );
    } catch( ex ){
      setTimeout( throwException( ex ), 0);
    }
  }

  function callSubscriberWithImmediateExceptions( subscriber, message, data ){
    subscriber( message, data );
  }

  function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions ){
    var subscribers = messages[matchedMessage],
      callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions,
      s;

    if ( !messages.hasOwnProperty( matchedMessage ) ) {
      return;
    }

    for (s in subscribers){
      if ( subscribers.hasOwnProperty(s)){
        callSubscriber( subscribers[s], originalMessage, data );
      }
    }
  }

  function createDeliveryFunction( message, data, immediateExceptions ){
    return function deliverNamespaced(){
      var topic = String( message ),
        position = topic.lastIndexOf( '.' );

      // deliver the message as it is now
      deliverMessage(message, message, data, immediateExceptions);

      // trim the hierarchy and deliver message to each level
      while( position !== -1 ){
        topic = topic.substr( 0, position );
        position = topic.lastIndexOf('.');
        deliverMessage( message, topic, data, immediateExceptions );
      }
    };
  }

  function messageHasSubscribers( message ){
    var topic = String( message ),
      found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])),
      position = topic.lastIndexOf( '.' );

    while ( !found && position !== -1 ){
      topic = topic.substr( 0, position );
      position = topic.lastIndexOf( '.' );
      found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic]));
    }

    return found;
  }

  function publish( message, data, sync, immediateExceptions ){
    var deliver = createDeliveryFunction( message, data, immediateExceptions ),
      hasSubscribers = messageHasSubscribers( message );

    if ( !hasSubscribers ){
      return false;
    }

    if ( sync === true ){
      deliver();
    } else {
      setTimeout( deliver, 0 );
    }
    return true;
  }

  /**
   *  PubSub.publish( message[, data] ) -> Boolean
   *  - message (String): The message to publish
   *  - data: The data to pass to subscribers
   *  Publishes the the message, passing the data to it's subscribers
  **/
  PubSub.publish = function( message, data ){
    return publish( message, data, false, PubSub.immediateExceptions );
  };

  /**
   *  PubSub.publishSync( message[, data] ) -> Boolean
   *  - message (String): The message to publish
   *  - data: The data to pass to subscribers
   *  Publishes the the message synchronously, passing the data to it's subscribers
  **/
  PubSub.publishSync = function( message, data ){
    return publish( message, data, true, PubSub.immediateExceptions );
  };

  /**
   *  PubSub.subscribe( message, func ) -> String
   *  - message (String): The message to subscribe to
   *  - func (Function): The function to call when a new message is published
   *  Subscribes the passed function to the passed message. Every returned token is unique and should be stored if
   *  you need to unsubscribe
  **/
  PubSub.subscribe = function( message, func ){
    if ( typeof func !== 'function'){
      return false;
    }

    // message is not registered yet
    if ( !messages.hasOwnProperty( message ) ){
      messages[message] = {};
    }

    // forcing token as String, to allow for future expansions without breaking usage
    // and allow for easy use as key names for the 'messages' object
    var token = 'uid_' + String(++lastUid);
    messages[message][token] = func;
    
    // return token for unsubscribing
    return token;
  };

  /* Public: Clears all subscriptions
   */
  PubSub.clearAllSubscriptions = function clearAllSubscriptions(){
    messages = {};
  };

  /*Public: Clear subscriptions by the topic
  */
  PubSub.clearSubscriptions = function clearSubscriptions(topic){
    var m; 
    for (m in messages){
      if (messages.hasOwnProperty(m) && m.indexOf(topic) === 0){
        delete messages[m];
      }
    }
  };

  /* Public: removes subscriptions.
   * When passed a token, removes a specific subscription.
   * When passed a function, removes all subscriptions for that function
   * When passed a topic, removes all subscriptions for that topic (hierarchy)
   *
   * value - A token, function or topic to unsubscribe.
   *
   * Examples
   *
   *    // Example 1 - unsubscribing with a token
   *    var token = PubSub.subscribe('mytopic', myFunc);
   *    PubSub.unsubscribe(token);
   *
   *    // Example 2 - unsubscribing with a function
   *    PubSub.unsubscribe(myFunc);
   *
   *    // Example 3 - unsubscribing a topic
   *    PubSub.unsubscribe('mytopic');
   */
  PubSub.unsubscribe = function(value){
    var isTopic    = typeof value === 'string' && messages.hasOwnProperty(value),
      isToken    = !isTopic && typeof value === 'string',
      isFunction = typeof value === 'function',
      result = false,
      m, message, t;

    if (isTopic){
      delete messages[value];
      return;
    }

    for ( m in messages ){
      if ( messages.hasOwnProperty( m ) ){
        message = messages[m];

        if ( isToken && message[value] ){
          delete message[value];
          result = value;
          // tokens are unique, so we can just stop here
          break;
        }

        if (isFunction) {
          for ( t in message ){
            if (message.hasOwnProperty(t) && message[t] === value){
              delete message[t];
              result = true;
            }
          }
        }
      }
    }

    return result;
  };
  PubSub.debugGetMessages=function(){
    return messages;
  };
}));
